﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;
namespace Z2.Scripting
{

	/// <summary>
	/// Scripting Engine
	/// </summary>
	public class ScriptingEngine
	{

		#region Members

		private const string SCRIPT_CLASS = "Script";
		private CSharpCodeProvider m_compiler;
		private CompilerParameters m_params;
		private readonly Dictionary<Guid, ScriptEntry> m_sources;

		#endregion

		#region Constructor

		/// <summary>
		/// Constructor
		/// </summary>
		public ScriptingEngine()
		{
			this.m_sources = new Dictionary<Guid, ScriptEntry>();
			this.Initialize();
		}

		#endregion

		#region Methods

		/// <summary>
		/// Initializes scripting egine - creates instance of compiler, creates params, etc.
		/// </summary>
		private void Initialize()
		{
			Debug.WriteLine("Creating compiler..");

			IDictionary<string, string> options = new Dictionary<string, string>();
			options.Add("CompilerVersion", "v2.0");
			this.m_compiler = new CSharpCodeProvider(options);

			this.m_params = new CompilerParameters
			{
				GenerateExecutable = false,
				GenerateInMemory = true
			};

			Debug.WriteLine("Adding references..");
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				try
				{
					string location = assembly.Location;
					if (!string.IsNullOrEmpty(location) && File.Exists(location))
					{
						Debug.WriteLine(string.Format("Adding {0}..", location));
						this.m_params.ReferencedAssemblies.Add(location);
					}
				}
				catch (NotSupportedException)
				{
					Debug.WriteLine(string.Format("Skipping assembly {0}.", assembly.FullName));
				}
			}
			//try
			//{
				//this.m_params.ReferencedAssemblies.Add("Z2.Core.dll");
				//if(!this.m_params.ReferencedAssemblies.Contains("Microsoft.Xna.Framework.dll"))
				//    this.m_params.ReferencedAssemblies.Add("Microsoft.Xna.Framework.dll");
			//}
			
		}

		/// <summary>
		/// Clears cache (all compiled scripts)
		/// </summary>
		public void Clear()
		{
			foreach (var entry in this.m_sources)
			{
				GC.SuppressFinalize(entry.Value.Script);
			}
			GC.WaitForPendingFinalizers();
			this.m_sources.Clear();
		}

		/// <summary>
		/// Compiles script source defined in <paramref name="source"/> and stores it to the cache
		/// </summary>
		/// <param name="id">Uniquie identifier for the source</param>
		/// <param name="source">Script source code</param>
		private void Compile(Guid id, string source)
		{
			Debug.WriteLine("Compiling..");
			CompilerResults results = this.m_compiler.CompileAssemblyFromSource(this.m_params, new[] { source });
			
			Debug.WriteLine("Done.");
			if (results.Errors.HasErrors)
			{
				Debug.WriteLine("Got some errors, lol");
				StringBuilder builder = new StringBuilder();
				foreach (CompilerError error in results.Errors)
				{
					Debug.WriteLine(error.ErrorText);
					builder.AppendLine(Convert.ToString(error.ErrorText));
				}

				//
				// TODO: Make property bool ThrowIfError
				//

				ScriptingEngineException exception = new ScriptingEngineException(builder.ToString());
				exception.IsCompileError = true;

				throw exception;
				return;
			}
			
			try
			{
				IScriptBase instance = 
					results.CompiledAssembly.CreateInstance(ScriptingEngine.SCRIPT_CLASS) as IScriptBase;
				if (instance == null)
				{
					Debug.WriteLine("instance == null");
					throw new ScriptingEngineException("Couldn't retype Script class to IScriptBase.");
				}
				ScriptEntry entry = new ScriptEntry 
				{
					CompiledAssembly = results.CompiledAssembly, 
					Script = instance 
				};
				this.m_sources.Add(id, entry);
				Debug.WriteLine("Successfully compiled and added to cache.");
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex);
				throw new ScriptingEngineException("Couldn't create instance of Script class.", ex);
			}
		}

		/// <summary>
		/// Gets compiled class by it's identifier
		/// </summary>
		/// <param name="id">Unique identifier</param>
		/// <returns>Instance of <see cref="IScriptBase"/></returns>
		public IScriptBase GetScript(Guid id)
		{
			if (this.m_sources.ContainsKey(id))
				return this.m_sources[id].Script;
			return null;
		}

		/// <summary>
		/// Adds script source code, compiles it and stores to the cache
		/// </summary>
		/// <param name="source">Source</param>
		/// <returns>Identifier assigned to the script</returns>
		public Guid AddScript(string source)
		{
			string preview = source.Length > 64 ? source.Substring(0, 64) : source;
			Guid id = Guid.NewGuid();
			
			Debug.WriteLine(string.Format("Adding source with Guid:{0}", id.ToString("D")));

			this.Compile(id, source);

			return id;
		}

		#endregion

	}
}
